/*
 *  Copyright 2008 Mark Ashworth <javameme@gmail.com>.
 *
 *  Licensed under the GNU General Public License v3 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *       http://www.gnu.org/licenses/gpl-3.0.html
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 *  under the License.
 */
package metaphor.service.spring;

import org.springframework.security.acls.sid.Sid;
import org.springframework.security.acls.domain.AclImpl;
import org.springframework.security.acls.domain.BasePermission;
import org.springframework.security.acls.*;
import org.springframework.security.acls.objectidentity.ObjectIdentityImpl;
import org.springframework.security.acls.objectidentity.ObjectIdentity;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.annotation.Propagation;
import metaphor.service.NoAclServiceSecurityException;
import metaphor.service.SecurityGranted;

/**
 * A Spring Framework security manager
 *
 * @author Mark Ashworth <javameme@gmail.com>
 * @version 1.0.0
 * @since 1.0.0
 */
public class SpringSecurityManager implements metaphor.service.SecurityManager {

    private static final Permission[] PERMISSIONS = new Permission[] {
        BasePermission.READ,
        BasePermission.WRITE,
        BasePermission.CREATE,
        BasePermission.DELETE,
        BasePermission.ADMINISTRATION
    };

    private MutableAclService mutableAclService;

    /**
     * Returns the acl service
     * @return MutableAclService
     * @throws NoAclServiceSecurityException If there is no acl service defined
     */
    public MutableAclService getMutableAclService() throws NoAclServiceSecurityException {
        if (mutableAclService == null) throw new NoAclServiceSecurityException("The Acl Service is not defined");
        return mutableAclService;
    }

    public void setMutableAclService(MutableAclService mutableAclService) {
        this.mutableAclService = mutableAclService;
    }

    /**
     * Retrieves or creates an Acl for the persistenClass and id
     * @param persistentClass The persistent class
     * @param id The object identifier
     * @return AclImpl
     * @throws metaphor.service.SecurityException If there was an error
     */
    protected AclImpl fetchAcl(Class persistentClass, Long id) throws metaphor.service.SecurityException {
        return fetchAcl(new ObjectIdentityImpl(persistentClass, id));
    }

    /**
     * Retrieves or creates an Acl for the object identity
     * @param oid The object identity
     * @return AclImpl
     * @throws metaphor.service.SecurityException If there was an error
     */
    protected AclImpl fetchAcl(ObjectIdentity oid) throws metaphor.service.SecurityException {
        try {
            return (AclImpl) getMutableAclService().readAclById(oid);
        } catch (NotFoundException e) {
            return (AclImpl) getMutableAclService().createAcl(oid);
        }
    }

    /**
     * Retrieves or creates an Acl for the object identity and security identifier
     * @param oid The object identity
     * @param sid The security identifier
     * @return AclImpl
     * @throws metaphor.service.SecurityException If there was an error
     */
    protected AclImpl fetchAcl(ObjectIdentity oid, Sid sid) throws metaphor.service.SecurityException {
        try {
            return (AclImpl) getMutableAclService().readAclById(oid, new Sid[] {sid});
        } catch (NotFoundException e) {
            return (AclImpl) getMutableAclService().createAcl(oid);
        }
    }

    /**
     * Grants read permission to the user on the object defined by the persistentClass
     * and object identifier.<br />
     *
     * @param persistentClass The class to grant access to
     * @param id The object identifier of the persistent class
     * @param sid The security identity
     * @throws java.lang.SecurityException If there was an error
     */
    @Transactional(propagation=Propagation.REQUIRED, readOnly=false)
    public void grantRead(
            Class persistentClass,
            Long id,
            Sid sid) throws metaphor.service.SecurityException {

        grantPermission(persistentClass, id, sid, BasePermission.READ);
    }

    /**
     * Grants write permission to the user on the object defined by the persistentClass
     * and object identifier.<br />
     *
     * @param persistentClass The class to grant access to
     * @param id The object identifier of the persistent class
     * @param sid The security identity
     * @throws metaphor.service.SecurityException If there was an error
     */
    @Transactional(propagation=Propagation.REQUIRED, readOnly=false)
    public void grantWrite(
            Class persistentClass,
            Long id,
            Sid sid) throws metaphor.service.SecurityException {

        grantPermission(persistentClass, id, sid, BasePermission.WRITE);
    }

    /**
     * Grants create permission to the user on the object defined by the persistentClass
     * and object identifier.<br />
     *
     * @param persistentClass The class to grant access to
     * @param id The object identifier of the persistent class
     * @param sid The security identity
     * @throws metaphor.service.SecurityException If there was an error
     */
    @Transactional(propagation=Propagation.REQUIRED, readOnly=false)
    public void grantCreate(
            Class persistentClass,
            Long id,
            Sid sid) throws metaphor.service.SecurityException {

        grantPermission(persistentClass, id, sid, BasePermission.CREATE);
    }

    /**
     * Grants delete permission to the user on the object defined by the persistentClass
     * and object identifier.<br />
     *
     * @param persistentClass The class to grant access to
     * @param id The object identifier of the persistent class
     * @param sid The security identity
     * @throws metaphor.service.SecurityException If there was an error
     */
    @Transactional(propagation=Propagation.REQUIRED, readOnly=false)
    public void grantDelete(
            Class persistentClass,
            Long id,
            Sid sid) throws metaphor.service.SecurityException {

        grantPermission(persistentClass, id, sid, BasePermission.DELETE);
    }

    /**
     * Grants administration permission to the user on the object defined by the persistentClass
     * and object identifier.<br />
     *
     * @param persistentClass The class to grant access to
     * @param id The object identifier of the persistent class
     * @param sid The security identity
     * @throws metaphor.service.SecurityException If there was an error
     */
    @Transactional(propagation=Propagation.REQUIRED, readOnly=false)
    public void grantAdministration(
            Class persistentClass,
            Long id,
            Sid sid) throws metaphor.service.SecurityException {

        grantPermission(persistentClass, id, sid, BasePermission.ADMINISTRATION);
    }

    /**
     * Grants all permissions to the user on the object defined by the persistentClass
     * and object identifier.<br />
     *
     * @param persistentClass The class to grant access to
     * @param id The object identifier of the persistent class
     * @param sid The security identity
     * @throws metaphor.service.SecurityException If there was an error
     */
    @Transactional(propagation=Propagation.REQUIRED, readOnly=false)
    public void grantAll(
            Class persistentClass,
            Long id,
            Sid sid) throws metaphor.service.SecurityException {

        grantPermission(persistentClass, id, sid, PERMISSIONS);
    }

    /**
     * Grants a permission to the user on the object defined by the persistentClass
     * and object identifier.<br />
     * 
     * @param persistentClass The class to grant access to
     * @param id The object identifier of the persistent class
     * @param sid The security identity to grant the permission to
     * @param permission The permission to grant to the user
     * @throws metaphor.service.SecurityException If there was an error
     */
    @Transactional(propagation=Propagation.REQUIRED, readOnly=false)
    public void grantPermission(
            Class persistentClass,
            Long id,
            Sid sid,
            Permission permission) throws metaphor.service.SecurityException {

        if (persistentClass == null) throw new java.lang.SecurityException("The persistentClass is null");
        if (id == null) throw new java.lang.SecurityException("The persistentClass identifier is null");
        if (sid == null) throw new java.lang.SecurityException("The sid is null");
        if (permission == null) throw new java.lang.SecurityException("The permission is null");

        AclImpl acl = fetchAcl(persistentClass, id);

        boolean granted;
        try {
            granted = acl.isGranted(new Permission[]{permission}, new Sid[] {sid}, false);
        } catch (NotFoundException e) {
            granted = false;
        }

        if (!granted) {
            acl.insertAce(acl.getEntries().length, permission, sid, true);
        }

        getMutableAclService().updateAcl(acl);
    }

    /**
     * Grants a set of permissions to the user on the object defined by the persistentClass
     * and object identifier.<br />
     *
     * @param persistentClass The class to grant access to
     * @param id The object identifier of the persistent class
     * @param sid The security identity to grant the permission to
     * @param permissions The permissions to grant to the user
     * @throws metaphor.service.SecurityException If there was an error
     */
    @Transactional(propagation=Propagation.REQUIRED, readOnly=false)
    public void grantPermission(
            Class persistentClass,
            Long id,
            Sid sid,
            Permission[] permissions) throws metaphor.service.SecurityException {

        if (persistentClass == null) throw new java.lang.SecurityException("The persistentClass is null");
        if (id == null) throw new java.lang.SecurityException("The persistentClass identifier is null");
        if (sid == null) throw new java.lang.SecurityException("The sid is null");
        if (permissions == null) throw new java.lang.SecurityException("The permission is null");

        Sid[] sids = new Sid[] {sid};
        AclImpl acl = fetchAcl(persistentClass, id);

        boolean granted;
        for (Permission permission : permissions) {
            if (permission != null) {
                try {
                    granted = acl.isGranted(new Permission[]{permission}, sids, false);
                } catch (NotFoundException e) {
                    granted = false;
                }

                if (!granted) {
                    acl.insertAce(acl.getEntries().length, permission, sids[0], true);
                }
            }
        }
        getMutableAclService().updateAcl(acl);
    }

    /**
     * Denies a permission to the user on the object defined by the persistentClass
     * and object identifier.<br />
     *
     * @param persistentClass The class to grant access to
     * @param id The object identifier of the persistent class
     * @param sid The security identity to deny the permission to
     * @param permission The permission to grant to the user
     * @throws metaphor.service.SecurityException If there was an error
     */
    @Transactional(propagation=Propagation.REQUIRED, readOnly=false)
    public void denyPermission(
            Class persistentClass,
            Long id,
            Sid sid,
            Permission permission) throws metaphor.service.SecurityException {

        if (persistentClass == null) throw new java.lang.SecurityException("The persistentClass is null");
        if (id == null) throw new java.lang.SecurityException("The persistentClass identifier is null");
        if (sid == null) throw new java.lang.SecurityException("The sid is null");
        if (permission == null) throw new java.lang.SecurityException("The permission is null");

        AclImpl acl = fetchAcl(persistentClass, id);

        AccessControlEntry[] entries = acl.getEntries();
        for (int index = 0; index < entries.length; index++ ) {
            AccessControlEntry ace = entries[index];

            if ((ace.getPermission().getMask() == permission.getMask()) && ace.getSid().equals(sid)) {
                if (ace.isGranting()) {
                    acl.deleteAce(index);
                    acl.insertAce(index, permission, sid, false);
                }
            }
        }
        getMutableAclService().updateAcl(acl);
    }

    @Transactional(propagation=Propagation.REQUIRED, readOnly=true)
    public Sid getOwner(
            Class persistentClass,
            Long id) throws metaphor.service.SecurityException {

        if (persistentClass == null) throw new java.lang.SecurityException("The persistentClass is null");
        if (id == null) throw new java.lang.SecurityException("The persistentClass identifier is null");

        AclImpl acl = fetchAcl(persistentClass, id);
        return acl.getOwner();
    }

    /**
     * Change the owner of a persistent object.<br />
     * @param persistentClass The persistent class
     * @param id The object identifier of the persistent class object
     * @param sid The security identity of the new owner
     * @throws metaphor.service.SecurityException If there was an error
     */
    @Transactional(propagation=Propagation.REQUIRED, readOnly=false)
    public void changeOwnership(
            Class persistentClass,
            Long id,
            Sid sid) throws metaphor.service.SecurityException {

        if (persistentClass == null) throw new java.lang.SecurityException("The persistentClass is null");
        if (id == null) throw new java.lang.SecurityException("The persistentClass identifier is null");
        if (sid == null) throw new java.lang.SecurityException("The sid is null");

        AclImpl acl = fetchAcl(persistentClass, id);
        acl.setOwner(sid);

        getMutableAclService().updateAcl(acl);
    }

    /**
     * Determines authorization.  The order of the <code>permission</code> and <code>sid</code> arguments is
     * <em>extremely important</em>! The method will iterate through each of the <code>permission</code>s in the order
     * specified. For each iteration, all of the <code>sid</code>s will be considered, again in the order they are
     * presented. A search will then be performed for the first {@link org.springframework.security.acls.AccessControlEntry} object that directly
     * matches that <code>permission:sid</code> combination. When the <em>first full match</em> is found (ie an ACE
     * that has the SID currently being searched for and the exact permission bit mask being search for), the grant or
     * deny flag for that ACE will prevail. If the ACE specifies to grant access, the method will return
     * <code>true</code>. If the ACE specifies to deny access, the loop will stop and the next <code>permission</code>
     * iteration will be performed. If each permission indicates to deny access, the first deny ACE found will be
     * considered the reason for the failure (as it was the first match found, and is therefore the one most logically
     * requiring changes - although not always). If absolutely no matching ACE was found at all for any permission,
     * the parent ACL will be tried (provided that there is a parent and {@link org.springframework.security.acls.Acl#isEntriesInheriting()} is
     * <code>true</code>. The parent ACL will also scan its parent and so on. If ultimately no matching ACE is found,
     * a <code>NotFoundException</code> will be thrown and the caller will need to decide how to handle the permission
     * check. Similarly, if any of the SID arguments presented to the method were not loaded by the ACL,
     * <code>UnloadedSidException</code> will be thrown.
     *
     * @param persistentClass The persistent class
     * @param id The object identifier of the persisten class object
     * @param permissions the exact permissions to scan for (order is important)
     * @param sids the exact SIDs to scan for (order is important)
     * @param administrativeMode if <code>true</code> denotes the query is for administrative purposes and no auditing
     *        will be undertaken
     *
     * @return SecurityGranted.GRANTED, DENIED, NOTFOUND
     *
     * @throws NotFoundException if an exact ACE for one of the permission bit masks and SID combination could not be
     *         found
     * @throws org.springframework.security.acls.UnloadedSidException if the passed SIDs are unknown to this ACL because the ACL was only loaded for a
     *         subset of SIDs
     */
    public SecurityGranted isGranted(
            Class persistentClass,
            Long id,
            Permission[] permissions,
            Sid[] sids,
            boolean administrativeMode) throws metaphor.service.SecurityException {

        return isGranted(fetchAcl(persistentClass, id), permissions, sids, administrativeMode);
    }

    /**
     * Determines authorization.  The order of the <code>permission</code> and <code>sid</code> arguments is
     * <em>extremely important</em>! The method will iterate through each of the <code>permission</code>s in the order
     * specified. For each iteration, all of the <code>sid</code>s will be considered, again in the order they are
     * presented. A search will then be performed for the first {@link org.springframework.security.acls.AccessControlEntry} object that directly
     * matches that <code>permission:sid</code> combination. When the <em>first full match</em> is found (ie an ACE
     * that has the SID currently being searched for and the exact permission bit mask being search for), the grant or
     * deny flag for that ACE will prevail. If the ACE specifies to grant access, the method will return
     * <code>true</code>. If the ACE specifies to deny access, the loop will stop and the next <code>permission</code>
     * iteration will be performed. If each permission indicates to deny access, the first deny ACE found will be
     * considered the reason for the failure (as it was the first match found, and is therefore the one most logically
     * requiring changes - although not always). If absolutely no matching ACE was found at all for any permission,
     * the parent ACL will be tried (provided that there is a parent and {@link org.springframework.security.acls.Acl#isEntriesInheriting()} is
     * <code>true</code>. The parent ACL will also scan its parent and so on. If ultimately no matching ACE is found,
     * a <code>NotFoundException</code> will be thrown and the caller will need to decide how to handle the permission
     * check. Similarly, if any of the SID arguments presented to the method were not loaded by the ACL,
     * <code>UnloadedSidException</code> will be thrown.
     *
     * @param acl The access control list to check if the permission is granted
     * @param permissions the exact permissions to scan for (order is important)
     * @param sids the exact SIDs to scan for (order is important)
     * @param administrativeMode if <code>true</code> denotes the query is for administrative purposes and no auditing
     *        will be undertaken
     *
     * @return SecurityGranted.GRANTED, DENIED, NOTFOUND
     *
     * @throws NotFoundException if an exact ACE for one of the permission bit masks and SID combination could not be
     *         found
     * @throws org.springframework.security.acls.UnloadedSidException if the passed SIDs are unknown to this ACL because the ACL was only loaded for a
     *         subset of SIDs
     *
     * @throws metaphor.service.SecurityException If there was an error checking granted
     */
    public SecurityGranted isGranted(
            Acl acl,
            Permission[] permissions,
            Sid[] sids,
            boolean administrativeMode) throws metaphor.service.SecurityException {

        try {
            if (acl.isGranted(permissions, sids, administrativeMode)) {
                return SecurityGranted.GRANTED;
            } else {
                return SecurityGranted.DENIED;
            }
        } catch (NotFoundException e) {
            return SecurityGranted.NOTFOUND;
        }
    }
}
